テスト 思考垂れ流し
目次
hr.icon
ポイント
テスト界隈にはモックを積極的に使う派(ロンドン学派)と使わない派(デトロイト学派、古典派)がある。
デトロイト派閥でやるなら、クリーンアーキテクチャの内側からテストするのが良さげ
デトロイト派のデメリットもしっかり認識しておくこと
基本的なテストと実装の時系列は以下が良いんじゃないかなと
単体テストをする/しないの判断
TDDはテスト手法ではなく生産性向上の手法と考えている
統合テストを重視した方が品質は上がりやすいとかなんとか
良い単体テストには条件がある
思考
(デトロイト派)データベースは共有依存だからモックすべき?
結局、テストはどういう分類方法が良いんだろうなぁ
システムが小さく初期の場合は、統合テスト中心でいい...DB系のテストケースを単体テストと統合テストで2回やんのは厳しい...戦意が削がれる。それなら統合テストだけでやっちゃおう...
pytestならmark.parametrizeで保守性は上げれる可能性あるから、頑張ってみい
ポイント
hr.icon
テスト界隈にはモックを積極的に使う派(ロンドン学派)と使わない派(デトロイト学派、古典派)がある。
ロンドン学派:
テスト対象の依存関係は、不変的なもの(Enum, Const)以外、全てモックに置き換える
「単体」の定義は、クラスを指すのであり、単体クラスで完結できないテストは全て統合テストと捉える。
コードを検証する。(ホワイトボックステストみたいな感じかな?onigiri.w2.icon)
動作の内部の隅から隅までチェック。
デトロイト学派(古典派):
テスト対象の依存関係は、共有依存関係だけモックにする
共有依存関係 = ユニットテスト間で状態が共有されるもの
例えば、データベースなんかは、モックしない場合ユニットテスト間で状態を共有することになりやすい。
共有せずに毎ユニットテストごとに初期化するっていう手もあるが、デメリットと比較して選択すべき。
「単体」の定義は振る舞い。つまり、同時に複数のクラスがまたがってもいい。(モックしないし)
動作(振る舞い)を検証する。
用意されたメニュー・契約を従ってるかどうかだけチェック。
2つ以上の動作を検証する場合は「統合テスト」として扱う。
参考
デトロイト派閥でやるなら、クリーンアーキテクチャの内側からテストするのが良さげ
デトロイト派の方針としては、「ドメインモデルから実装及びテストを書いていき、レイヤーを重ねていくアプローチ」を取る。
あの依存関係のまま、外側にテストを広げていく感じ。
で、外側でのテストの際には、内の実装コードをモックせずに利用するのが原則。
私もこっちの方法に賛成するonigiri.w2.icon
1. モック作るのしんどい
2. モックによってメインコードの変更検知ができなくなる可能性あり
モックの一番の問題は、本番とテストで違うコードが走ることで、これは自動テストの価値をだいぶ下げてしまう。テストが通っているのは、コードが正しいのか、コードがモックと揃っているからなのかわからなくなる。
もう一つの問題は、モックと実装が密結合してしまうことで、後々コードを変更するのが大変になる。実装が変えやすいようにテストを書くのに、実装を変えづらくなっては本末転倒だ。
デトロイト派のデメリットもしっかり認識しておくこと
レイヤが重なっていって結合数が増えた場合、テスト失敗の際にどこが原因かを見つけにくい可能性がある
あれ、これしか思いつかないな...orz
まあ、他にも外部システムに依存する場合、環境によって動作が変わる可能性があるとかありそう。
だけど...そこは「外部プロセス依存」みたいに捉えられそうであり、「モック化する」か「統合テストに任せる」になりそうだね。
統合テストの場合、本番と同じ環境でテストするイメージなので、環境が変わるってことはないやろし。
基本的なテストと実装の時系列は以下が良いんじゃないかなと
時系列案
1. 外部から利用される際のタッチポイント(ex: API、ユースケース、...)を洗い出し、それらのテストケースを作成
これはできるなら、コードで先に作成しておくのが良いかと
なお、タッチポイントとかは、事前設計の段階で粗方固まっていると想定。
もちろん実装の最中に変更がある可能性もあるが、それは仕方ない。
2. クリーンアーキテクチャの中心部分から、TDDを使って実装していく テストケース作成 -> まずはRed -> 次にメインをテストに合わせる形で実装してGreen -> その後にメインコードをちゃんと書いてRed, Greenを調整 -> 最後はAll Greenにする。
point.icon ただし、全部が全部、単体テスト書かなくても良いかなぁって思う
3. 外部システムはモックにし内部のタプルは全て実物を使った上で、「1.」のテストを実装して実行
4. All Greenになるよう修正
5. 外部システムも全て実物にした上で「1.」のユースケースをテスト?
6. All Greenになるよう修正
所感
3, 5は一緒でも良い可能性ある。つまり、モックを用意しない。
3はいわゆる「内部結合テスト」で、5は「外部結合テスト」ってやつだな。
テストは実装の動作品質だけじゃなく、「セキュリティ」「パフォーマンス」っていう観点もあるはず。
ここら辺はどこでテストするんだろうな。
単体テストをする/しないの判断
table:
協力オブジェクトの数: 少ない 協力オブジェクトの数: 多い
コードの複雑さ/ドメインにおける重要性: 低い 取るにとらないコード コントローラ
コードの複雑さ/ドメインにおける重要性: 高い ドメインモデル・アルゴリズム 過度に複雑なコード
取るに足りないコード: 単体テストしなくていい
ドメインモデル・アルゴリズム:単体テスト頑張ろう
コントローラ(API関数などだな):統合テストに譲ろう
これユースケースも当たるのかな?
過度に複雑なコード:設計が危うい。分割できないか考えなおせ。
コントローラーが複雑になるのは最悪仕方ないって意見もあった。
ただ、ビジネスロジックがコントローラーに含まれるのだけは避けたいな
それすると、コントローラーが重要なテスト対象になってしまう。
つまり、単体テストの必要性が出てくる。それはしんどいことになりそう。
TDDはテスト手法ではなく生産性向上の手法と考えている
単体テストを書いても意味ないっていう意見もあるけど、自分はTDDがあるなら意味ありそうって思う。 コードを書く際に仕様を意識せずに書くと右往左往して開発スピードが遅くなる。自分場合は。。。
コードに自信が持てないし、何を作れば良いんだっけ?ってなる時もある。
だから、先に仕様としてテストケースを洗い出しつつ、All Greenになるように開発するのがスピード高そう。
けど、それがスピード遅いってんなら多分必要ないかもしれない。統合テストを重視した方がいいということもあるかも。
統合テストを重視した方が品質は上がりやすいとかなんとか
テストトロフィーって考え方があるんだって。
もはや、システムが小規模なら多分統合テストだけで良いんじゃない?
品質も賄えるし、開発速度も上がるよ多分。
統合テストの結果がGreenになるまで、リファクタリングや修正を繰り返せばOKってだけよ。
小規模ならそれで間に合いそうって思う。
統合テスト時の観点
統合テストの定義
1つのシステム(プロセス)で決められたユースケースとそれに付随する挙動が期待された通りになるかテスト
外部結合、内部結合っていう分け方があるにはありそう。
外部結合 = 外部システム/プロセスはモックで代替
内部結合 = 外部システムは本物使う
以下、観点
1. 入力/出力に関連するテスト
全ての正常入力に対して正常レスポンスが返るか
異常入力に対して異常レスポンスが返るか
2. 副作用(外部システム/プロセス)に関連するテスト
正常入力に対して正しい副作用(データ保存、外部API叩くなど)がされるか
異常入力に対して正しい副作用がされるか
memo.icon
副作用ってのは、APIサーバー(プロセス)とは別のプロセス/システムに対する操作のこと。正しくいくのかって話。
ここではモックを使ってもいいし/使わなくてもいい。まあ最後はモックを使わずに本物でテストをするのが安全でしかないけど。
3. セキュリティ系
権限のないリクエストに対して正しいレスポンスを返すか
4. 内部の突然の異常に対して適切な挙動をするか
トランザクションは効いてるか
ユーザーには適切なレスポンスを返してるか
その他不整合に繋がる状況は起きないようになってるか
その他なんかありそう。
良い単体テストには条件がある
1. 退行(リグレッション)に対する保護
つまり回帰テストとしてちゃんと使える状態になってるってこと。
回帰テストはまじで品質にとって大事。特にシステムを変更する際に効いてくる。 これがあるとシステム変更の際のバグ検出率が一気に上がる。むしろこれないと不安まである。
2. リファクタリングへの耐性があるか
SUT(テスト対象)の仕様が変わってないが、内部が変わっただけで失敗するテストはだめ。
テストでは、SUTの内部を触らないこと。
常に仕様(出力/結果/副作用)に対してテストを書く。
そうすれば仕様変更しない限りテストが壊れない。すぐ壊れるテストは脆いテストと言う。 なお、副作用も無くせるならできる限り無くしたいね。。。
副作用はバグを生み出しやすいから。
3. テスト時間は短く
単体テストはすぐに結果がわかるようにしたい。
ビルドの度に動かしたいレベル。
すぐ結果を得ることで開発の品質をすぐに把握し修正スピードを上げれる。
4. 保守がしやすい
テストコードの意味がわかる。
どう言うテストしてるのかわかる。
理解しやすい。変更しやすい。
テストしやすい。
思考
hr.icon
データベースは共有依存だからモックすべき?
onigiri.w2.iconの考え
主張としては「基本的にはモックしなくて良いんじゃない?」と思ってる。
共有依存は、テストケース間でその状態に依存するってこと。
つまり、あるテストケース実施が、その状態を通じて他テストケースに影響し得る可能性があることを指してる。
たしかに、DBは普通に使ったら共有依存に当たるだろう。
けど、テストケースごとにDBの内容を初期化しておけば、別にテストケース間で状態を共有することにはならないと考える。
毎回、真っ新な状態にしておき、各テストケースが始まる際に必要な状態を作っておく。
これだと共有依存にはならないはず。なのでDB用にモックを作る必要もないかなと。
反対意見や懸念
テスト速度の問題
各テストケースごとにDBの初期化やクリーンナップをしてたんじゃ、時間がかかりすぎる可能性あり。
onigiri.w2.icon
確かにシステムが大きくなっていき、テストケースの数が増えれば増えるほど問題が顕著になりそう
ただ小規模なうち、問題が顕著化する/しそうになるまでは良いんじゃない?とか思ったりする
初期化/クリーンアップ処理が複雑じゃない?問題
onigiri.w2.icon
これは現代のORマッパーとか使ってたら行けるんじゃない?とか思ったりしてるけど...
正味、問題になりそうではある。
リソース逼迫するんじゃない?問題
onigiri.w2.icon
これも小規模なうちは顕在化しない問題。
大きくなってくるとやばそう。統合テストでしかやっちゃダメになる可能性あり。
結局、テストはどういう分類方法が良いんだろうなぁ
Googleのテストサイズ的な考え方か。
Small: 単一のプロセスで完了するテスト
Medium: 単一のマシンで完了するテスト
Large: 制限なし。外部システムとのやりとりまで含めて全部
統合テスト、単体テストって考え方か。(これは曖昧すぎるけど)
単体テスト: 古典派定義の単体テスト
統合テスト(内部): サブシステムのみのテスト。外部システムは全てモック化
統合テスト(外部):外部システムも全て本物で対象サブシステムをテスト
全体テスト:E2Eでやる感じかな。End to End。
onigiri.w2.icon
まあ、なんにせよ、分類の定義と役割を決めてあげれば良い話
一旦は以下でいいんじゃない?
Small: 単一プロセスで完了するやつ(単体テストと思えばいい。けど古典派で)
モック化するのは原則、共有依存と外部プロセス依存のみ
Medium:単一マシンで完了するやつ(統合テスト的な役割にできる)
Large:制限なし(本番と完璧に同様の状況でテスト。マニュアルテストじゃね?これ)